﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using Google.GData.Contacts;
using Google.GData.Client;
using Google.GData.Extensions;
using Google.Contacts;
using Outlook = Microsoft.Office.Interop.Outlook;

namespace GoogleSync
{
    class ContactSync
    {


        public static String HOME = "C:\\outlook\\";
        private static String REF_STORE = HOME + "reference.dat";

        Dictionary<String, GoogleContact> googleContacts = new Dictionary<String, GoogleContact>();

        Dictionary<String, OutContact> outlookContacts = new Dictionary<String, OutContact>();

        LinkedList<ReferenceContact> referenceContacts = new LinkedList<ReferenceContact>();

        RequestSettings reqSettings;
        ContactsRequest cr;
        ContactsRequest crPicture;

        private Outlook.MAPIFolder folder;
        private static String folderLocation;
        private static String username;
        private static String password;
        private Outlook.Application app;


        String googleGroup;

        public ContactSync(Outlook.Application myApp)
        {
            app = myApp;
        }

        public static String getFolderPath(){
            return folderLocation;
        }

        /**
         * @param args
         */
        public static void main(Outlook.Application myApp, String folderLocationIn, String usernameIn, String passwordIn)
        {
            folderLocation = folderLocationIn;
            username = usernameIn;
            password = passwordIn;
            if (!Directory.Exists(HOME))
            {
                Directory.CreateDirectory(HOME);
            }
            ContactSync me = new ContactSync(myApp);
            me.retrieveOutlookContacts();
            me.retrieveGoogleContacts();
            me.retrieveReferenceContacts();
            me.sync();
            me.saveReferenceContacts();
        }

        private void saveReferenceContacts()
        {
            FileStream fo = new FileStream(REF_STORE, FileMode.OpenOrCreate, FileAccess.Write);
            BinaryFormatter bf = new BinaryFormatter();
            bf.Serialize(fo, referenceContacts);
            fo.Flush();
            fo.Close();
        }

        private void retrieveReferenceContacts()
        {
            if (File.Exists(REF_STORE))
            {
                FileStream fi = new FileStream(REF_STORE, FileMode.Open, FileAccess.Read);
                BinaryFormatter bf = new BinaryFormatter();
                referenceContacts = (LinkedList<ReferenceContact>)bf.Deserialize(fi);
                fi.Close();
            }
        }

        private void sync()
        {
            // Cycle through the reference contacts. Their should be a Google and Outlook
            // contact for each. If there isn't, it's been deleted, so delete the others.
            ReferenceContact[] cArray = new ReferenceContact[referenceContacts.Count];
            referenceContacts.CopyTo(cArray, 0);
            foreach (ReferenceContact referenceContact in cArray)
            {
                GoogleContact googleContact = null;
                try
                {
                    googleContact = googleContacts[referenceContact.getGoogleContactID()];
                    googleContacts.Remove(referenceContact.getGoogleContactID());
                }
                catch (KeyNotFoundException) { }
                OutContact outContact = null;
                try
                {
                    outContact = outlookContacts[referenceContact.getOutlookContactID()];
                    outlookContacts.Remove(referenceContact
                        .getOutlookContactID());
                }
                catch (KeyNotFoundException) { }
                if ((googleContact == null || googleContact.getDeleted()) && outContact != null)
                {
                    // Google contact was null, deleted. Delete Outlook.
                    log("DELETING OUTLOOK: " + outContact.getFirstName() + " "
                            + outContact.getLastName());
                    outContact.delete();
                }
                if (outContact == null && googleContact != null)
                {
                    // Outlook contact was null, deleted. Delete Google.
                    log("DELETING GOOGLE: " + googleContact.getFirstName() + " "
                            + googleContact.getLastName());
                    googleContact.delete();
                }
                if (googleContact == null || outContact == null)
                {
                    // In the case of a delete, also delete the reference contact.
                    referenceContacts.Remove(referenceContact);
                }
                else
                {
                    // Otherwise do a field by field sync.
                    syncContacts(referenceContact, googleContact, outContact);
                }
            }
            // What's left? Google Contacts with no reference contact. 
            // Add reference, and Outlook contact.
            foreach (GoogleContact googleContact in googleContacts.Values)
            {
                // But check if there is an outlook contact we can merge with first, in case
                // we lost the reference store somehow. 
                String outID = googleContact.getField(Fields.OUTID);
                OutContact oc = null;
                if (outID != null)
                {
                    try
                    {
                        oc = outlookContacts[outID];
                        outlookContacts.Remove(outID);
                    }
                    catch (KeyNotFoundException) { }
                }
                if (oc == null)
                {
                    // No outlook contact to merge with... just add it.
                    log("ADDING to OUTLOOK " + googleContact.getFirstName() + " "
                            + googleContact.getLastName());
                    OutContact outContact = new OutContact(folder, app);
                    ReferenceContact contact = new ReferenceContact();
                    copyContact(googleContact, outContact, contact);
                    updateReferenceIDs(googleContact, outContact, contact);
                    referenceContacts.AddLast(contact);
                }
                else
                {
                    // If both contacts exist, based on ouklook ID, then merge them. 
                    // Keep the Google version - this could be an option later.
                    log("MERGING " + googleContact.getFirstName() + " "
                            + googleContact.getLastName());
                    ReferenceContact reference = new ReferenceContact();
                    reference.updateWith(oc, reference);
                    reference.setGoogleContactID(googleContact.getGoogleContactID());
                    reference.setOutlookContactID(oc.getOutlookContactID());
                    referenceContacts.AddLast(reference);
                    syncContacts(reference, googleContact, oc);
                }
            }
            // What's left? Outlook Contacts with no reference contact. 
            // Add reference, and Google contact.
            foreach (OutContact outContact in outlookContacts.Values)
            {
                log("ADDING to GOOGLE  " + outContact.getFirstName() + " "
                        + outContact.getLastName());
                GoogleContact googleContact = new GoogleContact(cr, googleGroup);
                ReferenceContact contact = new ReferenceContact();
                copyContact(outContact, googleContact, contact);
                updateReferenceIDs(googleContact, outContact, contact);
                referenceContacts.AddLast(contact);
            }
        }

        private void copyContact(AContact source, AContact dest,
                ReferenceContact reference)
        {
            dest.updateWith(source, reference);
            reference.updateWith(source, reference);
            dest.save();
        }

        private void updateReferenceIDs(GoogleContact googleContact,
                OutContact outContact, ReferenceContact reference)
        {
            reference.setGoogleContactID(googleContact.getGoogleContactID());
            reference.setOutlookContactID(outContact.getOutlookContactID());
            reference.setGooglePictureHash(googleContact.getPictureHash());
            reference.setOutPictureHash(outContact.getPictureHash());
        }

        private void syncContacts(ReferenceContact reference,
                GoogleContact googleContact, OutContact outContact)
        {
            foreach (Fields field in Enum.GetValues(typeof(Fields)))
            {
                if (!reference.getField(field).Equals(outContact.getField(field)))
                {
                    log("Updating GOOGLE  " + field + " for "
                            + outContact.getFirstName() + " "
                            + outContact.getLastName() + " from "
                            + googleContact.getField(field) + " to "
                            + outContact.getField(field));
                    reference.setField(field, outContact.getField(field));
                    googleContact.setField(field, outContact.getField(field));
                }
                else if (!reference.getField(field).Equals(
                        googleContact.getField(field)))
                {
                    log("Updating OUTLOOK " + field + " for "
                            + outContact.getFirstName() + " "
                            + outContact.getLastName() + " from "
                            + outContact.getField(field) + " to "
                            + googleContact.getField(field));
                    reference.setField(field, googleContact.getField(field));
                    outContact.setField(field, googleContact.getField(field));
                }
            }
            if (!reference.getGooglePictureHash().Equals(
                    googleContact.getPictureHash()))
            {
                outContact.setPictureBytes(googleContact.getPictureBytes(reference.getGooglePictureHash()));
                log("Updating OUTLOOK Picture for " + outContact.getFirstName()
                        + " " + outContact.getLastName() + " from "
                        + outContact.getPictureHash() + " to "
                        + googleContact.getPictureHash());
            }
            googleContact.save();
            outContact.save();
            reference.setOutPictureHash(outContact.getPictureHash());
            reference.setGooglePictureHash(googleContact.getPictureHash());
        }

        private void retrieveGoogleContacts()
        {
            reqSettings = new RequestSettings("LongHome-ContactSync-2", username, password);
            reqSettings.AutoPaging = true;
            cr = new ContactsRequest(reqSettings);
            crPicture = new ContactsRequest(reqSettings);
            getContactsGroup();
            getAllContacts();
        }

        private void getContactsGroup()
        {
            Feed<Group> groupFeed = cr.GetGroups();
            foreach (Group entry in groupFeed.Entries)
            {
                String systemGroup = entry.SystemGroup;
                if (systemGroup != null
                        && systemGroup.Equals("Contacts"))
                {
                    googleGroup = entry.Id;
                }
            }
        }

        private void retrieveOutlookContacts()
        {
            // Outlook application
            //Outlook.Application outlookApplication = new Outlook.Application();
            if (folderLocation == null)
            {
                folder = app.Session.PickFolder();
                folderLocation = folder.FolderPath;
            }
            else
            {
                folder = getFolder(app.Session.Folders, folderLocation);
            }
            Outlook.Items folderItems = folder.Items;
            foreach (Object item in folderItems)
            {
                if (item is Outlook.ContactItem)
                {
                    Outlook.ContactItem contactItem = (Outlook.ContactItem)item;
                    if (contactItem.FullName != null && contactItem.FullName.Length > 0)
                    {
                        OutContact outContact = new OutContact(contactItem, app);
                        outlookContacts.Add(outContact.getOutlookContactID(),
                                outContact);
                    }
                }
            }
        }

        private Outlook.MAPIFolder getFolder(Outlook.Folders folders, String path){
            foreach (Outlook.MAPIFolder a in folders)
            {
                if(a.FolderPath.Equals(path)){
                    return a;
                }else if(path.StartsWith(a.FolderPath + "\\")){
                    return getFolder(a.Folders, path);
                }
            }
            return null;
        }


        public void getAllContacts()
        {
            ContactsQuery query = new ContactsQuery(ContactsQuery.CreateContactsUri("default"));
            query.ShowDeleted = false;
            query.NumberToRetrieve = 20000;

            Feed<Contact> feed = cr.Get<Contact>(query);            // Request the feed
            foreach (Contact entry in feed.Entries)
            {
                foreach(GroupMembership membership in entry.GroupMembership){
                    if(membership.HRef.Equals(googleGroup)){
                        GoogleContact googleContact = new GoogleContact(entry, cr);
                        this.googleContacts.Add(googleContact.getGoogleContactID(),
                                googleContact);
                        break;
                    }
                }
            }
        }

        private static StreamWriter logWriter;


        public static void log(String logString)
        {
            if (logWriter == null)
            {
                logWriter = new StreamWriter(HOME + "sync"
                        + String.Format("{0:yyyyMMddHHmmssFFF}", DateTime.Now) + ".log");
            }
            String output = String.Format("{0:u}", DateTime.Now) + ": " + logString
                    ;
            output = output.Replace("\r", "\\r");
            output = output.Replace("\n", "\\n");
            output += "\r\n";
            Console.Out.Write(output);
            logWriter.Write(output, 0, output.Length);
            logWriter.Flush();
        }
    }
}
